Skip to content

Conversation

@ameeshaagrawal
Copy link
Collaborator

@ameeshaagrawal ameeshaagrawal commented Jul 25, 2025

Summary by CodeRabbit

  • New Features

    • ID-based switchboards (including Message & CCTP), new trigger API, off‑chain simulation, and ERC‑20 style credits with permit, per‑chain max fees and disconnect support.
    • Expanded multi‑chain coverage and deployment/ops tooling (deployment, verify, upgrade, rescue/whitelist, admin scripts).
  • Refactor

    • Public surface moved from address‑based to ID/bytes32 identifiers; payload identifiers and proof formats updated; gas/limits made configurable.
  • Tests

    • Large new test suites covering protocol, switchboards, utilities, and app gateways.
  • Chores

    • New deployment manifests, labels, config artifacts and a version bump.

tHeMaskedMan981 and others added 24 commits September 23, 2025 00:25
Fix expire bid and event in schedule precompile
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 50

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (10)
contracts/evmx/helpers/AsyncPromise.sol (1)

144-155: Revert handler failure must not revert the whole flow

current behavior: If revertHandlerSelector is unset or the revert handler call fails, _handleRevert reverts. This rolls back state updates in markResolved/markOnchainRevert, bricking the promise and preventing retries. This should be non‑reverting with explicit state and/or events.

-    function _handleRevert(bytes32 payloadId_) internal {
-        bytes memory combinedCalldata = abi.encodePacked(
-            revertHandlerSelector,
-            abi.encode(revertHandlerData, payloadId_)
-        );
-
-        (bool success, , ) = localInvoker.tryCall(0, gasleft(), 0, combinedCalldata);
-
-        if (!success) {
-            // todo: in this case, promise will stay unresolved
-            revert PromiseRevertFailed();
-        }
-    }
+    function _handleRevert(bytes32 payloadId_) internal {
+        // No handler set: leave state as *_REVERTING and return for retry logic.
+        if (revertHandlerSelector == bytes4(0)) {
+            return;
+        }
+        bytes memory combinedCalldata = abi.encodePacked(
+            revertHandlerSelector,
+            abi.encode(revertHandlerData, payloadId_)
+        );
+        (bool success, , ) = localInvoker.tryCall(0, gasleft(), 0, combinedCalldata);
+        if (!success) {
+            // Do not revert; keep state in *_REVERTING to allow retries by watcher.
+            // Consider emitting an event here for observability.
+            return;
+        }
+    }

Optional: emit events RevertHandlerCalled/RevertHandlerFailed for ops visibility.

hardhat-scripts/s3Config/buildConfig.ts (1)

32-33: Replace EVMX_CHAIN_ID with a valid ChainSlug
EVMX_CHAIN_ID is a numeric evmChainId from getCurrentModeConfig(), not a slug. Casting it to ChainSlug breaks rpcKeys/wssRpcKeys and all per-slug maps, leading to “Missing RPC” at runtime. Convert the chainId to its slug (or export an EVMX_CHAIN_SLUG) before using it in supportedChainSlugs and evmxChainSlug.

contracts/evmx/fees/FeesManager.sol (1)

101-111: Over‑blocking bug: availability ignores already blocked credits

You compare balanceOf(consumeFrom_) to credits_ but ignore userBlockedCredits. This allows blocking more than the user’s free balance. This should enforce available = balance - blocked and prevent double‑blocking per request.

 function blockCredits(
@@
-        if (balanceOf(consumeFrom_) < credits_) revert InsufficientCreditsAvailable();
+        uint256 balance = balanceOf(consumeFrom_);
+        uint256 blocked = userBlockedCredits[consumeFrom_];
+        if (balance < blocked + credits_) revert InsufficientCreditsAvailable();
+        // prevent double-block on the same request
+        if (requestBlockedCredits[requestCount_] != 0) revert AlreadyBlocked();
@@
-        userBlockedCredits[consumeFrom_] += credits_;
+        userBlockedCredits[consumeFrom_] += credits_;

And declare:

error AlreadyBlocked();
contracts/evmx/base/AppGatewayBase.sol (1)

241-249: Approve on token contract, not FeesManager, before withdrawCredits

This approve targets FeesManager as if it were a token. It must approve on token_ so FeesManager can pull funds.

-    IERC20(address(feesManager__())).approve(address(feesManager__()), type(uint256).max);
+    IERC20(token_).approve(address(feesManager__()), type(uint256).max);
     feesManager__().withdrawCredits(chainSlug_, token_, amount_, maxFees, receiver_);
hardhat-scripts/deploy/6.connect.ts (1)

142-165: Also validate switchboardId when building EVMx configs

This should prevent pushing invalid configs to the watcher.

-          const switchboardId = addr[Contracts.FastSwitchboardId];
+          const switchboardId = addr[Contracts.FastSwitchboardId];
+          if (!switchboardId || switchboardId === "0") {
+            throw new Error(`FastSwitchboardId not found for chain ${chain}`);
+          }
hardhat-scripts/admin/disconnect.ts (1)

49-56: Fix isConfigSetOnSocket argument: pass switchboardId (string), not address

This should pass a switchboardId string. Using an address breaks the config check and causes unnecessary disconnects.

Apply:

-  const switchboard = addr[Contracts.FastSwitchboard];
-  checkIfAddressExists(switchboard, "Switchboard");
+  const switchboard = addr[Contracts.FastSwitchboard];
+  checkIfAddressExists(switchboard, "Switchboard");
+  // Disconnected state uses switchboardId "0"
+  const switchboardId = "0";
 ...
-  if (
-    await isConfigSetOnSocket(plug, socket, BYTES32_ZERO, switchboard, chain)
-  ) {
+  if (await isConfigSetOnSocket(plug, socket, BYTES32_ZERO, switchboardId, chain)) {
     console.log(`${plugContract} Socket Config  on ${chain} already set!`);
     return;
   }

Based on learnings (utils/appConfig.ts signature).

contracts/evmx/watcher/RequestHandler.sol (1)

115-120: Guard against invalid callType in getPrecompileFees

Calling an interface on address(0) will revert with a low-level error. This should validate the precompile exists and revert with a domain error.

 function getPrecompileFees(
     bytes4 callType_,
     bytes memory precompileData_
 ) external view returns (uint256) {
-    return precompiles[callType_].getPrecompileFees(precompileData_);
+    IPrecompile pc = precompiles[callType_];
+    if (address(pc) == address(0)) revert InvalidCallType();
+    return pc.getPrecompileFees(precompileData_);
 }
contracts/evmx/fees/Credit.sol (3)

137-152: deposit: double-mint bug and wrong credit/native accounting on withdraw failure

On withdraw failure, this mints creditAmount_ again, zeroes nativeAmount_, then adds zero to creditAmount_. This should mint nativeAmount_ credits instead and reflect in event values.

-        // Mint tokens to the user
-        _mint(depositTo_, creditAmount_);
+        // Mint credits to the user
+        _mint(depositTo_, creditAmount_);
 ...
-            if (!success) {
-                _mint(depositTo_, creditAmount_);
-                nativeAmount_ = 0;
-                creditAmount_ += nativeAmount_;
-            }
+            if (!success) {
+                // Compensate native payout with credits
+                _mint(depositTo_, nativeAmount_);
+                creditAmount_ += nativeAmount_;
+                nativeAmount_ = 0;
+            }

Ensure tokenOnChainBalances semantics remain consistent with how payouts are later debited.


198-209: Allowance check must ensure allowance >= amount, not just > 0

Current isCreditSpendable only checks allowance != 0. This should check allowance >= amount_ to avoid later revert inside super.transferFrom.

-        if (!_isWatcher(spender_) && consumeFrom_ != spender_) {
-            if (allowance(consumeFrom_, spender_) == 0) return false;
-        }
+        if (!_isWatcher(spender_) && consumeFrom_ != spender_) {
+            if (allowance(consumeFrom_, spender_) < amount_) return false;
+        }

271-283: createRequest ignores maxFees argument

maxFees passed by withdrawCredits is unused. This should set overrides with the provided maxFees_ (or the min of user cap and per-chain cap), not always getMaxFees(chainSlug_).

-    _setMaxFees(getMaxFees(chainSlug_));
+    _setMaxFees(maxFees_);

If you need a chain cap, enforce min(maxFees_, getMaxFees(chainSlug_)).

♻️ Duplicate comments (39)
hardhat-scripts/deploy/2.roles.ts (1)

99-103: WATCHER_ROLE not granted to MessageSwitchboard (bug).

The grant logic omits MessageSwitchboard from the condition that routes WATCHER_ROLE to the watcher address. This causes WATCHER_ROLE to be incorrectly granted to signer.address for MessageSwitchboard.

Apply this diff:

-        [Contracts.FastSwitchboard, Contracts.CCTPSwitchboard].includes(
+        [Contracts.FastSwitchboard, Contracts.CCTPSwitchboard, Contracts.MessageSwitchboard].includes(
           contractName as Contracts
         ) && roleName === ROLES.WATCHER_ROLE
src/finality.ts (1)

7-7: Past LOW=0 concern not addressed

The previous review flagged that LOW: 0 creates reorg exposure and falsy-defaulting bugs. This change from 1→0 remains unaddressed. Either document this as intentional "head-of-chain" semantics with explicit consumer guidance to use ?? instead of ||, or revert to 1 for safer defaults.

hardhat-scripts/verify/verify.ts (1)

119-135: Retry loop still re-verifies all contracts and sleeps unconditionally.

The issues flagged in past review comments remain:

  1. The loop doesn't narrow the pending set between retries—it keeps re-verifying chainParams instead of unverifiedChainParams.
  2. The sleep happens before checking if verification is complete.

Since the loop currently runs only once (line 119), these issues don't cause problems yet. However, if you increase the retry limit in the future, apply the fixes from the past review comments to avoid unnecessary work and delays.

hardhat-scripts/constants/types.ts (1)

17-17: Type switchboardId as BigNumberish for uint64 safety.

As noted in previous review, using string for a uint64 value is too permissive and can cause runtime errors. BigNumberish provides proper type safety for ethers.js calls.

Apply this diff:

+import { BigNumberish } from "ethers";
+
 export type AppGatewayConfig = {
   plugConfig: {
     appGatewayId: string;
-    switchboardId: string;
+    switchboardId: BigNumberish;
   };
   plug: string;
   chainSlug: number;
 };
hardhat-scripts/utils/address.ts (2)

58-65: Simplify toBytes32Format.

The function redundantly strips 0x prefix when toBytes32FormatHexString should always return a 0x-prefixed string after fixing the previous issue.

Apply this diff:

 export function toBytes32Format(hexString: string): number[] {
-  const hex32Format = toBytes32FormatHexString(hexString);
-  const cleanedHex32String = hex32Format.startsWith("0x")
-    ? hex32Format.slice(2)
-    : hex32Format;
-
-  return Array.from(Buffer.from(cleanedHex32String, "hex"));
+  const hex32 = toBytes32FormatHexString(hexString);
+  return Array.from(Buffer.from(hex32.slice(2), "hex"));
 }

42-56: Fix padding/validation for toBytes32FormatHexString.

The function has multiple edge-case issues:

  • Odd-length hex produces fractional offset, silently dropping nibbles
  • Non-hex input isn't validated
  • Inputs longer than 32 bytes aren't checked
  • Returns input unchanged for 64-char strings without ensuring 0x prefix

This should strictly validate hex, left-pad to 32 bytes, and always return 0x-prefixed output.

Apply this diff:

-export function toBytes32FormatHexString(hexString: string): string {
-  // this means that the string is already in bytes32 format with or without 0x prefix
-  if (hexString.length == 64 || hexString.length == 66) {
-    return hexString;
-  }
-  // Remove the '0x' prefix from the input string if it's present
-  const cleanedHexString = hexString.startsWith("0x")
-    ? hexString.slice(2)
-    : hexString;
-
-  const buffer = Buffer.alloc(32);
-  buffer.write(cleanedHexString, 32 - cleanedHexString.length / 2, "hex"); // each hex char is 2 bytes
-
-  return "0x" + buffer.toString("hex");
-}
+export function toBytes32FormatHexString(input: string): string {
+  // Normalize and strip 0x
+  let hex = input.startsWith("0x") ? input.slice(2) : input;
+  // Validate hex
+  if (!/^[0-9a-fA-F]*$/.test(hex)) {
+    throw new Error(`Invalid hex string: ${input}`);
+  }
+  // Enforce max size
+  if (hex.length > 64) {
+    throw new Error(`Hex string too long for bytes32 (got ${hex.length} nybbles)`);
+  }
+  // Enforce even length
+  if (hex.length % 2 !== 0) {
+    hex = "0" + hex;
+  }
+  // Left-pad to 32 bytes (64 nybbles)
+  return "0x" + hex.padStart(64, "0").toLowerCase();
+}
contracts/protocol/switchboard/SwitchboardBase.sol (1)

38-40: Add duplicate registration guard and event emission.

registerSwitchboard can be called multiple times, overwriting switchboardId without validation or notification. This should prevent duplicate registrations and emit an event for off-chain tracking.

Apply this diff:

+    event SwitchboardRegistered(uint64 indexed switchboardId);
+
     function registerSwitchboard() external onlyOwner {
+        require(switchboardId == 0, "Already registered");
         switchboardId = socket__.registerSwitchboard();
+        emit SwitchboardRegistered(switchboardId);
     }
hardhat-scripts/constants/constants.ts (1)

14-15: Document units for MSG_SB_FEES (keep string BigNumberish)

This should state wei units to prevent misuse. Keep string for BigNumberish compatibility.

-export const MSG_SB_FEES = "100000000";
+// Switchboard sibling-config fee (wei); keep as string for BigNumberish compatibility.
+export const MSG_SB_FEES = "100000000";
contracts/protocol/interfaces/ISwitchboard.sol (1)

19-26: Fix param docs order and document payable rationale

Docs list triggerId_ before plug_, but signature takes plug_ first. This should match the function to avoid integration errors. Also add a clear note why this is payable (fees/gas).

-    /**
-     * @notice Processes a trigger and creates payload
-     * @dev This function is called by the socket to process a trigger
-     * @dev sb can override this function to add additional logic
-     * @param triggerId_ Trigger ID from socket
-     * @param plug_ Source plug address
-     * @param payload_ Payload data
-     * @param overrides_ Overrides for the trigger
-     */
+    /**
+     * @notice Processes a trigger and creates payload.
+     * @dev Called by Socket; switchboards can implement custom logic.
+     * @dev Payable to collect switchboard fees and/or gas prepayment; implementers must document exact requirements.
+     * @param plug_ Source plug address
+     * @param triggerId_ Trigger ID from Socket
+     * @param payload_ Payload data
+     * @param overrides_ Overrides for the trigger
+     */
package.json (1)

25-25: Guard .env and avoid relying on executable bit

The trace script will fail when .env is absent and is shell‑specific. This should guard the source and invoke via bash for portability.

-    "trace": "source .env && bash trace.sh",
+    "trace": "bash -lc '[[ -f .env ]] && source .env; bash trace.sh'",
hardhat-scripts/utils/overrides.ts (1)

7-7: defaultType still forces legacy transactions (type 0).

The hardcoded defaultType = 0 at Line 7 forces legacy transactions on all networks where an explicit type isn't set. This prevents EIP-1559 transactions on networks that prefer or require them. The getOverrides function at Line 112 still uses this default.

Remove the default type to let ethers infer the appropriate transaction type:

-const defaultType = 0;
 
-  const { gasLimit, type = defaultType } = overrides;
+  const { gasLimit, type } = overrides;

Also applies to: 112-112

script/counter/DeployEVMxCounterApp.s.sol (1)

19-19: Fees remain hardcoded at 1 ether.

This was previously flagged. Fees should be sourced from an environment variable (e.g., FEES_WEI) to avoid misconfiguration across networks/forks.

-        uint256 fees = 1 ether;
+        uint256 fees = vm.envUint("FEES_WEI");
contracts/protocol/SocketConfig.sol (1)

123-132: Address the interface mismatch in connect() function.

The past review comment is still valid. The function signature doesn't match the ISocket interface. The interface expects connect(bytes32 appGatewayId_, address socket_, uint64 switchboardId_) but this implementation omits the socket_ parameter.

Apply this diff to fix the signature:

-    function connect(bytes32 appGatewayId_, uint64 switchboardId_) external override {
+    function connect(bytes32 appGatewayId_, address socket_, uint64 switchboardId_) external override {
         if (isValidSwitchboard[switchboardId_] != SwitchboardStatus.REGISTERED)
             revert InvalidSwitchboard();

-        PlugConfigEvm storage _plugConfig = _plugConfigs[msg.sender];
+        PlugConfigEvm storage _plugConfig = _plugConfigs[socket_];
         _plugConfig.appGatewayId = appGatewayId_;
         _plugConfig.switchboardId = switchboardId_;

-        emit PlugConnected(msg.sender, appGatewayId_, switchboardId_);
+        emit PlugConnected(socket_, appGatewayId_, switchboardId_);
     }
contracts/evmx/watcher/Watcher.sol (1)

183-201: tryCall error handling loses revert reason.

The past review comment correctly identified that the current implementation only checks the success boolean and discards the returnData from tryCall (lines 193-199). This makes debugging failures difficult since the actual revert reason from the called contract is lost.

test/evmx/AuctionManager.t.sol (1)

33-51: Test lacks outcome validation.

This test executes operations but doesn't verify results. The past review comments correctly identified:

  1. Missing balance/state checks after withdrawCredits (lines 39-46)
  2. Missing assertions after executeRequest calls to verify bid expiry and state changes (lines 47-51)

Without assertions, the test can't detect failures in the withdrawal or bid expiry logic.

script/helpers/TransferRemainingCredits.s.sol (1)

28-37: Missing validation before transfer.

The past review comment correctly identified that the script doesn't check if availableFees > 0 before attempting the transfer (lines 28-37). This results in unnecessary transaction costs for no-op transfers.

contracts/protocol/switchboard/FastSwitchboard.sol (2)

43-43: Changing visibility from external to public increases gas; keep external (or external virtual)

attest is only used externally; public adds unnecessary ABI thunk. This should be external (and virtual if overriding).

-    function attest(bytes32 digest_, bytes calldata proof_) public virtual {
+    function attest(bytes32 digest_, bytes calldata proof_) external virtual {

64-72: Empty processTrigger silently accepts ETH and does nothing

An external payable function with an empty body will consume gas/ETH and do nothing. This should be implemented, made abstract, or revert explicitly to prevent fund loss and logic gaps.

Options:

-    function processTrigger(
+    function processTrigger(
         address plug_,
         bytes32 triggerId_,
         bytes calldata payload_,
         bytes calldata overrides_
-    ) external payable virtual {}
+    ) external payable virtual {
+        revert("NotImplemented");
+    }

Or mark the contract abstract and declare the function without a body if derived contracts must implement it.

contracts/evmx/fees/FeesManager.sol (1)

51-67: Reinitializer bump requires an explicit upgrade/migration path

reinitializer(2) indicates an upgrade. Existing deployments will skip v1 initializer; new state must be safely initialized and storage layout preserved. This should include migration logic, upgrade tests, and documentation.

Add/point to:

  • Upgrade script that calls the new initializer post‑proxy upgrade.
  • A test that deploys v1, upgrades to v2, validates maxFeesPerChainSlug, sbType, feesPool, and owner/app gateway state.
contracts/protocol/SocketBatcher.sol (3)

13-19: Move IFastSwitchboard to interfaces file

Interfaces should live under interfaces/ for reuse and discoverability.

Create contracts/protocol/interfaces/IFastSwitchboard.sol and import it here.


47-66: Validate switchboardId resolves to a non-zero address before attest

This should guard against invalid IDs and clearer errors.

-        IFastSwitchboard(socket__.switchboardAddresses(switchboardId_)).attest(digest_, proof_);
+        address switchboard = socket__.switchboardAddresses(switchboardId_);
+        if (switchboard == address(0)) revert InvalidSwitchboard();
+        IFastSwitchboard(switchboard).attest(digest_, proof_);

76-104: CCTP: add zero-address check, try/catch around attest/prove, and syncOut only on success

This should improve safety and diagnostics; syncing on failure risks inconsistency.

-    address switchboard = socket__.switchboardAddresses(switchboardId_);
+    address switchboard = socket__.switchboardAddresses(switchboardId_);
+    if (switchboard == address(0)) revert InvalidSwitchboard();
...
-    ICCTPSwitchboard(switchboard).attestVerifyAndProveExecutions(
-        execParams_,
-        cctpParams_,
-        payloadId
-    );
+    try ICCTPSwitchboard(switchboard).attestVerifyAndProveExecutions(
+        execParams_,
+        cctpParams_,
+        payloadId
+    ) {
+        // ok
+    } catch Error(string memory reason) {
+        revert(string.concat("CCTP attest/prove failed: ", reason));
+    } catch {
+        revert("CCTP attest/prove failed");
+    }
...
-    ICCTPSwitchboard(switchboard).syncOut(payloadId, cctpParams_.nextBatchRemoteChainSlugs);
-    return (success, returnData);
+    if (success) {
+        ICCTPSwitchboard(switchboard).syncOut(payloadId, cctpParams_.nextBatchRemoteChainSlugs);
+    }
+    return (success, returnData);
contracts/evmx/base/AppGatewayBase.sol (3)

125-130: Validate request count in onDeployComplete to prevent TOCTOU

You encode watcher__().getCurrentRequestCount() but ignore it in the callback. This should validate the request count to avoid races between deploy and completion.

Apply:

-    function onDeployComplete(uint40, bytes calldata onCompleteData_) external virtual onlyWatcher {
+    function onDeployComplete(uint40 requestCount_, bytes calldata onCompleteData_) external virtual onlyWatcher {
+        require(requestCount_ <= watcher__().getCurrentRequestCount(), "Invalid request count");
         if (onCompleteData_.length == 0) return;
         uint32 chainSlug = abi.decode(onCompleteData_, (uint32));
         initializeOnChain(chainSlug);
     }

Also applies to: 373-377


145-150: Validate decoded deployed address before converting to bytes32

This should reject address(0) before toBytes32Format to avoid storing invalid forwarders.

 function setAddress(bytes memory data_, bytes memory returnData_) external onlyPromises {
     (uint32 chainSlug, bytes32 contractId) = abi.decode(data_, (uint32, bytes32));
-    forwarderAddresses[contractId][chainSlug] = asyncDeployer__().getOrDeployForwarderContract(
-        toBytes32Format(abi.decode(returnData_, (address))),
-        chainSlug
-    );
+    address deployed = abi.decode(returnData_, (address));
+    require(deployed != address(0), "Invalid deployed address");
+    forwarderAddresses[contractId][chainSlug] = asyncDeployer__().getOrDeployForwarderContract(
+        toBytes32Format(deployed),
+        chainSlug
+    );
 }

226-228: Guard against zero on-chain plug address

Registering a zero plug degrades watcher state and later calls.

 function _setValidPlug(bool isValid, uint32 chainSlug_, bytes32 contractId_) internal {
     bytes32 onchainAddress = getOnChainAddress(contractId_, chainSlug_);
+    require(onchainAddress != bytes32(0), "Invalid onchain address");
     watcher__().setIsValidPlug(isValid, chainSlug_, onchainAddress);
 }
hardhat-scripts/deploy/3.configureChains.ts (2)

50-57: Do not overwrite existing SwitchboardIdToAddressMap

This resets mappings and loses prior state. Initialize only when undefined.

-    let deployUtils: DeployParams = {
+    let deployUtils: DeployParams = {
       addresses: chainAddresses,
       mode,
       signer: signer,
       currentChainSlug: chain as ChainSlug,
     };
-    deployUtils.addresses.SwitchboardIdToAddressMap = {};
+    deployUtils.addresses.SwitchboardIdToAddressMap =
+      deployUtils.addresses.SwitchboardIdToAddressMap || {};

61-72: Validate returned fastSwitchboardId before storing

This should fail fast if registration/query returns zero/invalid.

   const fastSwitchboardId = await registerSb(
     chain,
     chainAddresses[Contracts.FastSwitchboard],
     signer,
     socketContract
   );
+  if (!fastSwitchboardId || fastSwitchboardId.toString() === "0") {
+    throw new Error(`Failed to register FastSwitchboard for chain ${chain}`);
+  }
   deployUtils.addresses[Contracts.FastSwitchboardId] =
     fastSwitchboardId.toString();
hardhat-scripts/deploy/6.connect.ts (1)

48-56: Validate switchboardId before using it

This should reject missing/zero IDs and prevent bad socket config calls.

-  const switchboardId = addr[Contracts.FastSwitchboardId];
+  const switchboardId = addr[Contracts.FastSwitchboardId];
+  if (!switchboardId || switchboardId === "0") {
+    throw new Error(`FastSwitchboardId not found for chain ${chain}`);
+  }
...
   const tx = await plug.functions[functionName](
     appGatewayId,
     socket.address,
     switchboardId,

Also applies to: 68-73

contracts/evmx/AuctionManager.sol (1)

235-246: then() inside async-wrapped function attaches to the previous promise.

With internal async, _postAsync() creates the new Promise after the function body. Calling then(...) here registers on the old promise.

Fix one of:

  • Move then(callbackSelector_, callbackData_) to be called immediately after invoking _createRequest(...) at the call sites.
  • Or extend the async modifier/post-hook to accept (selector, data) and call IPromise(watcher__().latestAsyncPromise()).then(selector, data) inside _postAsync().

Minimal change:

-        _setSchedule(delayInSeconds_);
-        then(callbackSelector_, callbackData_);
+        _setSchedule(delayInSeconds_);
+        // Attach .then(...) to the newly created promise after this function returns.
hardhat-scripts/deploy/1.deploy.ts (1)

122-124: Fix optional chaining check for feePool (regression).

feePool?.length == 0 skips deployment when feePool is undefined. This should deploy when missing or empty.

-      const feePool = getFeePool();
-      if (feePool?.length == 0) {
+      const feePool = getFeePool();
+      if (!feePool || feePool.length === 0) {
Errors.md (1)

52-56: Doc inconsistency: removed error name mismatch.

Summary claims removal of SwitchboardExistsOrDisabled(), table lists SwitchboardExists(). This should be consistent.

Update either the summary or the table so both reference the same removed error name/signature.

contracts/evmx/watcher/precompiles/WritePrecompile.sol (1)

183-186: Validate and mask payloadPointer-derived fields before use.

Direct shifts to uint40(...) without masking/validation can yield garbage and corrupt prevBatchDigestHash.

-        bytes32 prevBatchDigestHash = getPrevBatchDigestHash(
-            uint40(payloadParams.payloadPointer >> 120),
-            uint40(payloadParams.payloadPointer >> 80)
-        );
+        uint40 requestCount = uint40((payloadParams.payloadPointer >> 120) & bytes32(uint256((1 << 40) - 1)));
+        uint40 batchCount = uint40((payloadParams.payloadPointer >> 80) & bytes32(uint256((1 << 40) - 1)));
+        if (requestCount == 0) revert InvalidIndex(); // or a dedicated error
+        // Optionally validate batchCount against known bounds
+        bytes32 prevBatchDigestHash = getPrevBatchDigestHash(requestCount, batchCount);

Pick the appropriate error type for out-of-range values (e.g., a dedicated InvalidRequestCount()).

test/evmx/FeesTest.t.sol (1)

96-103: Add explicit post-disconnect assertions

This should assert the plug is disconnected before proceeding (e.g., getPlugConfig returns appGatewayId == 0). Without it, the test could pass due to unrelated conditions.

Apply this snippet after disconnectSocket():

 hoax(socketOwner);
 arbConfig.feesPlug.disconnectSocket();
+
+// Verify disconnection state
+{
+    (bytes32 appGatewayId_, uint64 switchboardId_) =
+        arbConfig.socket.getPlugConfig(address(arbConfig.feesPlug));
+    assertEq(appGatewayId_, bytes32(0), "appGatewayId should be zero after disconnect");
+    // If protocol semantics keep switchboardId, assert unchanged; else enforce zero:
+    // assertEq(switchboardId_, 0, "switchboardId should be zero after disconnect");
+}
contracts/utils/common/Structs.sol (1)

213-219: Bound dynamic arrays to prevent DoS in consumers

CCTPBatchParams and SolanaInstruction* structs expose unbounded arrays. This should have enforced limits in consuming contracts (batch sizes, accounts, args) with clear reverts to prevent gas griefing.

  • Define MAX_BATCH_SIZE, MAX_ACCOUNTS, MAX_ARGUMENTS constants and validate in entry points that accept these structs.
  • Validate related lengths (e.g., attestations/messages alignment, accountFlags length == accounts length, functionArgumentTypeNames length == functionArguments length).

Also applies to: 220-239

test/SetupTest.t.sol (2)

1158-1173: setupGatewayAndPlugs: remove double-iteration; build configs in one pass

This currently scans contractIds_ twice. Build configs once into a temp array and resize.

-        uint256 validPlugCount = _countValidPlugs(appGateway_, contractIds_, chainSlug_);
-        AppGatewayConfig[] memory configs = new AppGatewayConfig[](validPlugCount);
-        _populateConfigs(configs, appGateway_, contractIds_, chainSlug_);
-        if (validPlugCount > 0) {
-            watcherMultiCall(
-                address(configurations),
-                abi.encodeWithSelector(Configurations.setAppGatewayConfigs.selector, configs)
-            );
-        }
+        AppGatewayConfig[] memory tmp = new AppGatewayConfig[](contractIds_.length);
+        uint256 count = 0;
+        uint64 switchboardId = configurations.switchboards(chainSlug_, appGateway_.sbType());
+        for (uint i = 0; i < contractIds_.length; i++) {
+            bytes32 plug = appGateway_.getOnChainAddress(contractIds_[i], chainSlug_);
+            if (plug != bytes32(0)) {
+                tmp[count++] = AppGatewayConfig({
+                    plug: plug,
+                    chainSlug: chainSlug_,
+                    plugConfig: PlugConfigGeneric({
+                        appGatewayId: toBytes32Format(address(appGateway_)),
+                        switchboardId: switchboardId
+                    })
+                });
+            }
+        }
+        if (count > 0) {
+            AppGatewayConfig[] memory configs = new AppGatewayConfig[](count);
+            for (uint i = 0; i < count; i++) configs[i] = tmp[i];
+            watcherMultiCall(
+                address(configurations),
+                abi.encodeWithSelector(Configurations.setAppGatewayConfigs.selector, configs)
+            );
+        }

Also applies to: 1175-1186, 1188-1211


1105-1108: Fix abi.decode type: last tuple element is uint64 (switchboardId), not address

This currently causes a type mismatch and/or incorrect decoding.

-            (, Transaction memory transaction, , , , ) = abi.decode(
-                params.precompileData,
-                (address, Transaction, WriteFinality, uint256, uint256, address)
-            );
+            (, Transaction memory transaction, , , , ) = abi.decode(
+                params.precompileData,
+                (address, Transaction, WriteFinality, uint256, uint256, uint64)
+            );
contracts/evmx/watcher/RequestHandler.sol (2)

255-258: payloadPointer packing can overflow allocated bit ranges

requestCount_ and nextBatchCount must fit 40 bits; payloadCounter must fit 80 bits. Without checks/masking, fields can overflow and corrupt the pointer.

+        // Ensure values fit within 40/40/80 bits respectively
+        require(requestCount_ < (1 << 40), "Request count overflow");
+        require(nextBatchCount < (1 << 40), "Batch count overflow");
+        require(payloadCounter   < (1 << 80), "Payload counter overflow");
-        uint160 payloadPointer = (uint160(requestCount_) << 120) |
-            (uint160(nextBatchCount) << 80) |
-            uint160(payloadCounter++);
+        uint160 payloadPointer =
+            (uint160(requestCount_)   << 120) |
+            (uint160(nextBatchCount)  << 80)  |
+            uint160(payloadCounter++);

428-431: Capture and emit failure data from tryCall in onComplete

Only emitting RequestCompletedWithErrors loses error context. This should capture returnData and emit it.

-        if (r.onCompleteData.length > 0) {
-            (bool success, , ) = r.appGateway.tryCall(0, gasleft(), 0, r.onCompleteData);
-            if (!success) emit RequestCompletedWithErrors(requestCount_);
-        }
+        if (r.onCompleteData.length > 0) {
+            (bool success, bytes memory returnData, ) =
+                r.appGateway.tryCall(0, gasleft(), 0, r.onCompleteData);
+            if (!success) {
+                // Consider updating the event to include bytes returnData
+                emit RequestCompletedWithErrors(requestCount_);
+                // Optionally emit returnData via a new event to aid debugging
+            }
+        }
EventTopics.md (1)

41-45: Empty sections should be removed or populated.

These empty interface sections (IFastSwitchboard, IMessageSwitchboard, ICCTPSwitchboard) have been flagged in previous reviews. If these interfaces have no events to document, remove the sections; otherwise, populate them with the actual event information.

Comment on lines +230 to 234
function _permit(bytes memory feesApprovalData_) internal {
if (feesApprovalData_.length == 0) return;
(consumeFrom, , ) = feesManager__().approveAppGatewayWithSignature(feesApprovalData_);
(address spender, uint256 value, uint256 deadline, uint256 nonce, bytes memory signature) = abi.decode(feesApprovalData_, (address, uint256, uint256, uint256, bytes));
IERC20(address(feesManager__())).permit(spender, value, deadline, nonce, signature);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

_permit calls permit on FeesManager instead of token; decode token and target ERC20

Calling IERC20(address(feesManager__())).permit(...) is incorrect. Approval must be executed on the ERC20 token contract. Include token in feesApprovalData_ and call permit on that token.

 function _permit(bytes memory feesApprovalData_) internal {
     if (feesApprovalData_.length == 0) return;
-    (address spender, uint256 value, uint256 deadline, uint256 nonce, bytes memory signature) = abi.decode(feesApprovalData_, (address, uint256, uint256, uint256, bytes));
-    IERC20(address(feesManager__())).permit(spender, value, deadline, nonce, signature);
+    (address token, address spender, uint256 value, uint256 deadline, uint256 nonce, bytes memory signature)
+        = abi.decode(feesApprovalData_, (address, address, uint256, uint256, uint256, bytes));
+    IERC20(token).permit(spender, value, deadline, nonce, signature);
 }

Note: If your IERC20 doesn’t define permit, use your ERC20Permit interface instead. Based on learnings

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function _permit(bytes memory feesApprovalData_) internal {
if (feesApprovalData_.length == 0) return;
(consumeFrom, , ) = feesManager__().approveAppGatewayWithSignature(feesApprovalData_);
(address spender, uint256 value, uint256 deadline, uint256 nonce, bytes memory signature) = abi.decode(feesApprovalData_, (address, uint256, uint256, uint256, bytes));
IERC20(address(feesManager__())).permit(spender, value, deadline, nonce, signature);
}
function _permit(bytes memory feesApprovalData_) internal {
if (feesApprovalData_.length == 0) return;
(address token, address spender, uint256 value, uint256 deadline, uint256 nonce, bytes memory signature)
= abi.decode(feesApprovalData_, (address, address, uint256, uint256, uint256, bytes));
IERC20(token).permit(spender, value, deadline, nonce, signature);
}
🤖 Prompt for AI Agents
In contracts/evmx/base/AppGatewayBase.sol around lines 230-234, the _permit
function wrongly calls permit on the FeesManager; change the ABI decode to
include the token address (e.g., decode (address token, address spender, uint256
value, uint256 deadline, uint256 nonce, bytes memory signature)), then cast the
decoded token address to your ERC20Permit interface and call permit on that
token (not on feesManager__), keeping the early-return when
feesApprovalData_.length == 0; also use ERC20Permit type (or your IERC20 with
permit) to ensure the permit method exists.

Comment on lines +17 to +25
function decimals() external view returns (uint8);

function permit(
address spender,
uint256 value,
uint256 deadline,
uint256 nonce,
bytes memory signature
) external;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Non‑standard permit in IERC20 will break ERC‑2612 compatibility

This interface mixes core ERC‑20 with a non‑standard permit(address spender, uint256 value, uint256 deadline, uint256 nonce, bytes signature). ERC‑2612 defines:

  • permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
  • plus nonces(address) and DOMAIN_SEPARATOR().

Calling external tokens with this signature will revert. This should split into IERC20 (transfer/approve/etc.) and IERC20Permit (EIP‑2612), or drop permit here and use a separate interface.

Apply this diff to remove the non‑standard method:

 interface IERC20 {
@@
-    function permit(
-        address spender,
-        uint256 value,
-        uint256 deadline,
-        uint256 nonce,
-        bytes memory signature
-    ) external;

Add a separate IERC20Permit (new file) aligned to EIP‑2612:

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.21;

interface IERC20Permit {
    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external;

    function nonces(address owner) external view returns (uint256);
    function DOMAIN_SEPARATOR() external view returns (bytes32);
}
🤖 Prompt for AI Agents
In contracts/evmx/interfaces/IERC20.sol around lines 17 to 25, the interface
currently declares a non‑standard permit(spender,value,deadline,nonce,signature)
which breaks ERC‑2612 compatibility; remove that non‑standard permit declaration
from IERC20 so it only contains core ERC‑20 methods (decimals, transfer,
approve, allowance, balanceOf, totalSupply, etc.), and add a new file
contracts/evmx/interfaces/IERC20Permit.sol implementing the EIP‑2612 signature
(permit(owner,spender,value,deadline,v,r,s)) plus nonces(address) and
DOMAIN_SEPARATOR() as view functions; ensure SPDX/license and pragma lines match
project style and update any imports/usages to reference IERC20Permit where
EIP‑2612 behavior is required.

Comment on lines +65 to 66
requestCount = uint40(payloadParams.payloadPointer >> 120);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Duplicated bit extraction and no bounds check on requestCount

requestCount = uint40(payloadParams.payloadPointer >> 120) is repeated and relies on a specific bit layout. Silent truncation to uint40 can hide overflow or layout drift. This should centralize decoding and validate the range.

-        requestCount = uint40(payloadParams.payloadPointer >> 120);
+        requestCount = _extractRequestCount(payloadParams.payloadPointer);
...
-        requestHandler__().cancelRequestForReverts(uint40(payloadParams.payloadPointer >> 120));
+        requestHandler__().cancelRequestForReverts(_extractRequestCount(payloadParams.payloadPointer));

Add helper (outside the ranges shown):

function _extractRequestCount(uint256 payloadPointer) internal pure returns (uint40) {
    unchecked {
        uint256 shifted = payloadPointer >> 120; // [40 bits: requestCount][120 bits: rest]
        require(shifted <= type(uint40).max, "requestCount overflow");
        return uint40(shifted);
    }
}

Confirm the payloadPointer bit layout is stable and documented. If layout differs across versions, store requestCount explicitly rather than packing it.

Also applies to: 96-97


🏁 Script executed:

#!/bin/bash
# Locate duplicated bit extraction in PromiseResolver.sol
rg -n "uint40(payloadParams.payloadPointer >> 120)" -C2 contracts/evmx/watcher/PromiseResolver.sol

# Search for existing helper function named extractRequestCount
rg -n "extractRequestCount" -C2 .

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find the location of PromiseResolver.sol
fd PromiseResolver.sol

# Search for the bit extraction pattern more broadly
rg -n "payloadPointer >> 120" -C2

Length of output: 2212


Centralize requestCount extraction and add range check
Replace duplicated uint40(payloadParams.payloadPointer >> 120) at lines 65 and 96 with a helper and validate overflow:

function _extractRequestCount(uint256 payloadPointer) internal pure returns (uint40) {
    unchecked {
        uint256 shifted = payloadPointer >> 120;
        require(shifted <= type(uint40).max, "requestCount overflow");
        return uint40(shifted);
    }
}

Use _extractRequestCount(payloadParams.payloadPointer) for both the requestCount assignment and the cancelRequestForReverts call.

🤖 Prompt for AI Agents
In contracts/evmx/watcher/PromiseResolver.sol around lines 65 to 66 (and also
update the duplicate at line 96), the extraction of requestCount via repeated
uint40(payloadParams.payloadPointer >> 120) should be centralized: add an
internal pure helper named _extractRequestCount(uint256 payloadPointer) that
shifts payloadPointer right by 120, checks the shifted value does not exceed
type(uint40).max (require with "requestCount overflow"), and returns
uint40(shifted) (use unchecked for the cast), then replace the inline
uint40(...) at line 65 and the duplicate at line 96 with a call to
_extractRequestCount(payloadParams.payloadPointer).

Comment on lines 31 to 41
if (isAppGatewayCalled[params_.triggerId]) revert AppGatewayAlreadyCalled();

address appGateway = decodeAppGatewayId(params_.appGatewayId);
address appGateway = fromBytes32Format(params_.appGatewayId);
if (!configurations__.isValidPlug(appGateway, params_.chainSlug, params_.plug))
revert InvalidCallerTriggered();

feesManager__().transferCredits(appGateway, address(this), triggerFees);
IERC20(address(feesManager__())).transferFrom(appGateway, address(this), triggerFees);

triggerFromChainSlug = params_.chainSlug;
triggerFromPlug = params_.plug;
isAppGatewayCalled[params_.triggerId] = true;
(bool success, , ) = appGateway.tryCall(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix reentrancy/double-charge and enforce ERC20 transfer success

Setting isAppGatewayCalled only on success enables retries and allows reentrancy during appGateway.call to re-enter and double-charge. This should set the flag before the external call. Also, ERC20 transferFrom return value is ignored; tokens that return false won’t revert. Enforce success.

     function _callAppGateways(TriggerParams memory params_) internal {
         if (isAppGatewayCalled[params_.triggerId]) revert AppGatewayAlreadyCalled();
 
         address appGateway = fromBytes32Format(params_.appGatewayId);
         if (!configurations__.isValidPlug(appGateway, params_.chainSlug, params_.plug))
             revert InvalidCallerTriggered();
 
-        IERC20(address(feesManager__())).transferFrom(appGateway, address(this), triggerFees);
+        bool ok = IERC20(address(feesManager__())).transferFrom(appGateway, address(this), triggerFees);
+        if (!ok) revert TransferFailed(); // define TransferFailed() error in scope
 
         triggerFromChainSlug = params_.chainSlug;
         triggerFromPlug = params_.plug;
+        // prevent reentrancy/retries before external call
+        isAppGatewayCalled[params_.triggerId] = true;
         (bool success, , ) = appGateway.tryCall(
             0,
             gasleft(),
             0, // setting max_copy_bytes to 0 as not using returnData right now
             params_.payload
         );
 
         if (!success) {
             emit TriggerFailed(params_.triggerId);
         } else {
-            isAppGatewayCalled[params_.triggerId] = true;
             emit TriggerSucceeded(params_.triggerId);
         }
 
         triggerFromChainSlug = 0;
         triggerFromPlug = bytes32(0);
     }

If you prefer to support non‑standard ERC20s, replace the bool check with a safe wrapper (e.g., solady’s SafeTransferLib.safeTransferFrom).

Also applies to: 48-57

🤖 Prompt for AI Agents
In contracts/evmx/watcher/Trigger.sol around lines 31-41 (and similarly lines
48-57), the reentrancy/double-charge issue and unchecked ERC20 transfer are
present: set isAppGatewayCalled[params_.triggerId] = true before making the
external appGateway.call to prevent reentrancy and double charging, and enforce
that IERC20(feesManager__()).transferFrom(...) succeeds by checking the returned
bool (or use a safe transfer wrapper such as
SafeTransferLib.safeTransferFrom/solady) and revert on failure; apply the same
ordering and transfer success check in the other occurrence at lines 48-57.

Comment on lines +76 to 88
/// @notice Initializes the socket
/// @dev this function should be called even if deployed independently
/// to avoid ownership and permission exploit
/// @param appGatewayId_ The app gateway id
/// @param socket_ The socket address
/// @param switchboardId_ The switchboard id
function initSocket(
bytes32 appGatewayId_,
address socket_,
address switchboard_
uint64 switchboardId_
) external virtual socketInitializer {
_connectSocket(appGatewayId_, socket_, switchboard_);
_connectSocket(appGatewayId_, socket_, switchboardId_);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

initSocket lacks access control; first caller can permanently bind the plug

initSocket is external and only gated by socketInitializer. Any account can call it first and set socket_/appGatewayId_/switchboardId_, hijacking the plug. This should be owner/role‑gated or internal.

Options:

  • Gate with ownership/roles (preferred):
-    function initSocket(
+    function initSocket(
         bytes32 appGatewayId_,
         address socket_,
         uint64 switchboardId_
-    ) external virtual socketInitializer {
+    ) external virtual socketInitializer onlyOwner {
         _connectSocket(appGatewayId_, socket_, switchboardId_);
     }

Add an ownership base (e.g., Ownable2Step/OwnableRoles) in this contract or ensure every derived contract applies onlyOwner.

  • Or make it internal so derived contracts expose a guarded initializer:
-    function initSocket(
+    function _initSocket(
         bytes32 appGatewayId_,
         address socket_,
         uint64 switchboardId_
-    ) external virtual socketInitializer {
+    ) internal virtual socketInitializer {
         _connectSocket(appGatewayId_, socket_, switchboardId_);
     }

This should be fixed before release.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// @notice Initializes the socket
/// @dev this function should be called even if deployed independently
/// to avoid ownership and permission exploit
/// @param appGatewayId_ The app gateway id
/// @param socket_ The socket address
/// @param switchboardId_ The switchboard id
function initSocket(
bytes32 appGatewayId_,
address socket_,
address switchboard_
uint64 switchboardId_
) external virtual socketInitializer {
_connectSocket(appGatewayId_, socket_, switchboard_);
_connectSocket(appGatewayId_, socket_, switchboardId_);
}
/// @notice Initializes the socket
/// @dev this function should be called even if deployed independently
/// to avoid ownership and permission exploit
/// @param appGatewayId_ The app gateway id
/// @param socket_ The socket address
/// @param switchboardId_ The switchboard id
function initSocket(
bytes32 appGatewayId_,
address socket_,
uint64 switchboardId_
) external virtual socketInitializer onlyOwner {
_connectSocket(appGatewayId_, socket_, switchboardId_);
}
🤖 Prompt for AI Agents
In contracts/protocol/base/PlugBase.sol around lines 76-88, initSocket is
external and only protected by socketInitializer, allowing any first caller to
permanently bind the plug; change the function visibility or add ownership/role
checks: either (preferred) integrate an ownership mechanism (e.g.,
Ownable2Step/OwnableRoles) and require onlyOwner or a specific role to call
initSocket, or make initSocket internal and expose a separate externally
callable initializer in derived contracts that enforces owner/role gating;
update constructor/initializers and documentation accordingly so
socket/appGatewayId/switchboardId can only be set by the authorized account.

Comment on lines +48 to 64
function processTrigger(
address plug_,
bytes32 triggerId_,
bytes calldata payload_,
bytes calldata overrides_
) external payable override {
// Simple implementation that just accepts the trigger
// In a real switchboard, this would process the trigger
}

function registerSwitchboard() external {
socket__.registerSwitchboard();
function getTransmitter(
address sender_,
bytes32,
bytes calldata
) external pure returns (address) {
return sender_;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify mock matches ISwitchboard signature

processTrigger and getTransmitter signatures must match ISwitchboard. A mismatch compiles within the test suite if interfaces diverged, but will hide issues. This should confirm parity.


🏁 Script executed:

#!/bin/bash
# Verify ISwitchboard signatures
rg -n -C2 'interface\s+ISwitchboard\b' --type=sol
rg -n -C2 'processTrigger\(' --type=sol
rg -n -C2 'getTransmitter\(' --type=sol

Length of output: 83


🏁 Script executed:

#!/bin/bash
# Search all Solidity files for interface and function definitions
rg -nP 'interface\s+ISwitchboard' -g '*.sol'
rg -nP 'function\s+processTrigger' -g '*.sol'
rg -nP 'function\s+getTransmitter' -g '*.sol'

Length of output: 701


🏁 Script executed:

#!/bin/bash
# Print ISwitchboard interface signatures
rg -n -C2 'interface\s+ISwitchboard' -g 'contracts/protocol/interfaces/ISwitchboard.sol'
rg -n -C2 'function\s+processTrigger' -g 'contracts/protocol/interfaces/ISwitchboard.sol'
rg -n -C2 'function\s+getTransmitter' -g 'contracts/protocol/interfaces/ISwitchboard.sol'
echo "----"
# Print MockFastSwitchboard signatures
rg -n -C2 'function\s+processTrigger' -g 'test/mock/MockFastSwitchboard.sol'
rg -n -C2 'function\s+getTransmitter' -g 'test/mock/MockFastSwitchboard.sol'

Length of output: 1696


🏁 Script executed:

#!/bin/bash
# Extract processTrigger signature and surrounding for context
sed -n '25,50p' contracts/protocol/interfaces/ISwitchboard.sol
echo '---'
# Extract getTransmitter signature and surrounding for context
sed -n '38,60p' contracts/protocol/interfaces/ISwitchboard.sol

Length of output: 1097


Align mock to ISwitchboard signature

  • In test/mock/MockFastSwitchboard.sol, change
    function getTransmitter(address sender_, bytes32, bytes calldata) external pure returns (address)
    to
    function getTransmitter(address sender_, bytes32 payloadId_, bytes calldata transmitterSignature_) external view override returns (address)
    to match the interface’s mutability and override.
🤖 Prompt for AI Agents
In test/mock/MockFastSwitchboard.sol around lines 48 to 64, the mock's
getTransmitter signature does not match the ISwitchboard interface: it currently
declares parameters anonymously, is pure, and lacks override; update the
function signature to use named parameters (payloadId_ and
transmitterSignature_), change mutability to view, and add the override keyword
so it reads: function getTransmitter(address sender_, bytes32 payloadId_, bytes
calldata transmitterSignature_) external view override returns (address); ensure
the function body still returns sender_.

Comment on lines +154 to +166
function testFuzzLargeProofs(bytes32 digest, uint256 proofSize) public {
vm.assume(digest != bytes32(0));
vm.assume(proofSize >= 65 && proofSize <= 1000); // Reasonable proof sizes

// Create a large proof
bytes memory largeProof = new bytes(proofSize);
for (uint256 i = 0; i < proofSize; i++) {
largeProof[i] = bytes1(uint8(i % 256));
}

vm.expectRevert(InvalidSignature.selector);
fastSwitchboard.attest(digest, largeProof);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Missing import for InvalidSignature.

Line 164 references InvalidSignature.selector, but InvalidSignature is not imported. This will cause a compilation error.

Import the error from the appropriate contract. If it's defined in FastSwitchboard:

+import {FastSwitchboard, InvalidSignature} from "../../../contracts/protocol/switchboard/FastSwitchboard.sol";
-import {FastSwitchboard} from "../../../contracts/protocol/switchboard/FastSwitchboard.sol";

Or if it's in a common errors file, import from there. Verify the correct location and add the import statement.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function testFuzzLargeProofs(bytes32 digest, uint256 proofSize) public {
vm.assume(digest != bytes32(0));
vm.assume(proofSize >= 65 && proofSize <= 1000); // Reasonable proof sizes
// Create a large proof
bytes memory largeProof = new bytes(proofSize);
for (uint256 i = 0; i < proofSize; i++) {
largeProof[i] = bytes1(uint8(i % 256));
}
vm.expectRevert(InvalidSignature.selector);
fastSwitchboard.attest(digest, largeProof);
}
// at the top of test/protocol/switchboards/FastSwitchboardTest.t.sol
import {FastSwitchboard, InvalidSignature} from "../../../contracts/protocol/switchboard/FastSwitchboard.sol";
🤖 Prompt for AI Agents
In test/protocol/switchboards/FastSwitchboardTest.t.sol around lines 154-166,
the test references InvalidSignature.selector but the error type is not imported
which causes a compilation failure; add an import for the InvalidSignature error
from the contract that declares it (either import it from the FastSwitchboard
contract file if it's declared there, or from the shared/common errors file
where it lives), update the top-of-file import block to include that symbol, and
ensure the import path matches the project structure so the test compiles.

@@ -0,0 +1,88 @@
// SPDX-License-Identifier: GPL-3.0-only
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Remove " copy" from filename.

The filename MessageSwitchboardTest.t copy.sol contains a " copy" suffix, indicating this is likely an accidental duplicate or improperly renamed file. This will cause confusion and potential issues with build systems.

Rename the file to:

MessageSwitchboardTest.t.sol
🤖 Prompt for AI Agents
In test/protocol/switchboards/MessageSwitchboardTest.t copy.sol around line 1,
the filename includes an unintended " copy" suffix; rename the file to remove
the suffix so it becomes
test/protocol/switchboards/MessageSwitchboardTest.t.sol, update any
references/imports in the test suite or CI configs if present to the new
filename, and commit the rename so build systems and tooling no longer see the
duplicate.

Comment on lines +4 to +5
import "../../SetupTest.t.sol";
import {CounterPlug} from "../../apps/app-gateways/counter/MessageCounter.sol";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add missing MessageSwitchboard import.

The test references MessageSwitchboard.SiblingNotFound (line 67) and MessageSwitchboard.WatcherNotFound (line 85) but doesn't import MessageSwitchboard. This will cause compilation failures.

Add the import:

 import "../../SetupTest.t.sol";
 import {CounterPlug} from "../../apps/app-gateways/counter/MessageCounter.sol";
+import {MessageSwitchboard} from "../../../contracts/socket/switchboards/MessageSwitchboard.sol";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import "../../SetupTest.t.sol";
import {CounterPlug} from "../../apps/app-gateways/counter/MessageCounter.sol";
import "../../SetupTest.t.sol";
import {CounterPlug} from "../../apps/app-gateways/counter/MessageCounter.sol";
import {MessageSwitchboard} from "../../../contracts/socket/switchboards/MessageSwitchboard.sol";
🤖 Prompt for AI Agents
In test/protocol/switchboards/MessageSwitchboardTest.t copy.sol around lines
4-5, the file fails to import MessageSwitchboard even though the test uses
MessageSwitchboard.SiblingNotFound and MessageSwitchboard.WatcherNotFound; add
an import statement for the MessageSwitchboard contract at the top of the file
(import the MessageSwitchboard Solidity file from the protocol/switchboards
folder, e.g. ../../protocol/switchboards/MessageSwitchboard.sol) so the compiler
can resolve those referenced error types.

Comment on lines +36 to +41
emit TriggerProcessed(
optConfig.chainSlug,
msgSbFees,
_createDigest(digestParams),
digestParams
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify TriggerProcessed event import.

The test expects the TriggerProcessed event but there's no visible import or definition for it in this file. Confirm this event is properly imported from a parent contract or MessageSwitchboard.

Run the following script to verify the event definition:


🏁 Script executed:

#!/bin/bash
# Find TriggerProcessed event definition
rg -n "event TriggerProcessed" --type=sol

Length of output: 27


🏁 Script executed:

#!/bin/bash
rg -n "event TriggerProcessed" --glob "*.sol"

Length of output: 222


#!/bin/bash
# Locate MessageSwitchboardTest file(s)
fd MessageSwitchboardTest -e sol
# Inspect import statements in each test file
for f in $(fd MessageSwitchboardTest -e sol); do
  echo "---- $f ----"
  sed -n '1,50p' "$f"
done

🏁 Script executed:

#!/bin/bash
# Locate MessageSwitchboardTest file(s)
files=$(rg -l "MessageSwitchboardTest" -g 'test/protocol/switchboards/*.sol')
echo "$files"
for f in $files; do
  echo "---- $f ----"
  head -n 50 "$f"
  echo "--- Event definitions ---"
  rg -n "event TriggerProcessed" "$f" || echo "No event in $f"
  echo "--- Inheritance ---"
  rg -nP "contract\s+MessageSwitchboardTest\s+is\s+\w+" "$f" || echo "No inheritance found in $f"
done

Length of output: 888


#!/bin/bash
for f in test/protocol/switchboards/*.sol; do
  echo "---- $f ----"
  head -n 100 "$f"
done

#!/bin/bash
ls -1 test/protocol/switchboards
for f in test/protocol/switchboards/*.sol; do
  echo "---- $f ----"
  head -n 50 "$f"
done

🏁 Script executed:

#!/bin/bash
ls -1 test/protocol/switchboards
for f in test/protocol/switchboards/*.sol; do
  echo "---- $f ----"
  head -n 50 "$f"
done

Length of output: 3602


Add missing TriggerProcessed event declaration/import
This should import or redeclare the TriggerProcessed event so vm.expectEmit can locate it in test/protocol/switchboards/MessageSwitchboardTest.t copy.sol.

🤖 Prompt for AI Agents
In test/protocol/switchboards/MessageSwitchboardTest.t copy.sol around lines 36
to 41, the test emits TriggerProcessed but the event is not declared or imported
so vm.expectEmit cannot resolve it; add the TriggerProcessed event declaration
(matching the event signature used in production contracts) near the top of this
test file or import the contract/interface that defines TriggerProcessed, then
ensure the event signature exactly matches the emitted parameters so
vm.expectEmit can locate and compare the event.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

Comment on lines +221 to 229
address from_,
address to_,
uint256 amount_
) public override returns (bool) {
if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable();

if (isNonceUsed[consumeFrom][nonce]) revert NonceUsed();
isNonceUsed[consumeFrom][nonce] = true;
isApproved[consumeFrom][spender] = approval;
return (consumeFrom, spender, approval);
if (_isWatcher(msg.sender)) _approve(from_, msg.sender, amount_);
return super.transferFrom(from_, to_, amount_);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Whitelist RequestHandler as privileged spender to unblock RequestHandler.transferFrom

RequestHandler pulls credits during batch processing. Make it a privileged spender to avoid manual approvals.

-    if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable();
-
-    if (_isWatcher(msg.sender)) _approve(from_, msg.sender, amount_);
+    if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable();
+    // Allow Watcher and RequestHandler to spend without pre-approval
+    if (_isWatcher(msg.sender) || msg.sender == address(watcher__().requestHandler__())) {
+        _approve(from_, msg.sender, amount_);
+    }
     return super.transferFrom(from_, to_, amount_);

This pairs with the RequestHandler fee transfer path.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
address from_,
address to_,
uint256 amount_
) public override returns (bool) {
if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable();
if (isNonceUsed[consumeFrom][nonce]) revert NonceUsed();
isNonceUsed[consumeFrom][nonce] = true;
isApproved[consumeFrom][spender] = approval;
return (consumeFrom, spender, approval);
if (_isWatcher(msg.sender)) _approve(from_, msg.sender, amount_);
return super.transferFrom(from_, to_, amount_);
}
function transferFrom(
address from_,
address to_,
uint256 amount_
) public override returns (bool) {
if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable();
// Allow Watcher and RequestHandler to spend without pre-approval
if (_isWatcher(msg.sender) || msg.sender == address(watcher__().requestHandler__())) {
_approve(from_, msg.sender, amount_);
}
return super.transferFrom(from_, to_, amount_);
}

Comment on lines 124 to +136
// todo: can be changed to set the default gas limit for each chain
if (queueParams_.overrideParams.gasLimit == 0) {
queueParams_.overrideParams.gasLimit = 10000000;
if (queueParams_.transaction.chainSlug == 5000) {
// Mantle default gas limit
queueParams_.overrideParams.gasLimit = 8_000_000_000;
} else if (queueParams_.transaction.chainSlug == 1329) {
// Sei default gas limit
queueParams_.overrideParams.gasLimit = 8_000_000;
} else if (queueParams_.transaction.chainSlug == 999) {
// HyperEVM default gas limit
queueParams_.overrideParams.gasLimit = 1_500_000;
} else {
queueParams_.overrideParams.gasLimit = 10_000_000; // other chains default gas limit
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Mantle default gas limit is unrealistic (likely to revert).

8_000_000_000 gas will exceed typical block gas limits. This should be in the millions, not billions.

-            if (queueParams_.transaction.chainSlug == 5000) {
-                // Mantle default gas limit
-                queueParams_.overrideParams.gasLimit = 8_000_000_000;
+            if (queueParams_.transaction.chainSlug == 5000) {
+                // Mantle default gas limit
+                queueParams_.overrideParams.gasLimit = 8_000_000;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// todo: can be changed to set the default gas limit for each chain
if (queueParams_.overrideParams.gasLimit == 0) {
queueParams_.overrideParams.gasLimit = 10000000;
if (queueParams_.transaction.chainSlug == 5000) {
// Mantle default gas limit
queueParams_.overrideParams.gasLimit = 8_000_000_000;
} else if (queueParams_.transaction.chainSlug == 1329) {
// Sei default gas limit
queueParams_.overrideParams.gasLimit = 8_000_000;
} else if (queueParams_.transaction.chainSlug == 999) {
// HyperEVM default gas limit
queueParams_.overrideParams.gasLimit = 1_500_000;
} else {
queueParams_.overrideParams.gasLimit = 10_000_000; // other chains default gas limit
}
// todo: can be changed to set the default gas limit for each chain
if (queueParams_.overrideParams.gasLimit == 0) {
if (queueParams_.transaction.chainSlug == 5000) {
// Mantle default gas limit
queueParams_.overrideParams.gasLimit = 8_000_000;
} else if (queueParams_.transaction.chainSlug == 1329) {
// Sei default gas limit
queueParams_.overrideParams.gasLimit = 8_000_000;
} else if (queueParams_.transaction.chainSlug == 999) {
// HyperEVM default gas limit
queueParams_.overrideParams.gasLimit = 1_500_000;
} else {
queueParams_.overrideParams.gasLimit = 10_000_000; // other chains default gas limit
}
}
🤖 Prompt for AI Agents
In contracts/evmx/watcher/precompiles/WritePrecompile.sol around lines 123-136,
the Mantle default gasLimit is set to 8_000_000_000 (billions) which is
unrealistic and will likely revert; change that value to a sensible
million-scale default (e.g., 8_000_000) and keep the same numeric-formatting
style, review other chain defaults for correctness, and run tests/quick local
deploy to ensure the new default does not exceed block limits.

Comment on lines +197 to 200
// todo: remove after game
// also this overrides a payload deadline hence an unexecuted payload can
// be executed by new added transmitters. need to fix this by marking req deadline or something.
if (r.requestFeesDetails.winningBid.transmitter == bid_.transmitter)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove TODO and fix deadline override behavior

Reassigning transmitters currently “overrides a payload deadline” without safeguards. This should either:

  • Track a request-level deadline and prevent execution after expiry, or
  • Recompute deadlines deterministically per reassignment and enforce monotonicity.

Leave no TODOs in shipping code; implement one of the above and cover with tests.

Comment on lines 324 to 328
address watcherFeesPayer = r.requestFeesDetails.winningBid.transmitter == address(0)
? r.requestFeesDetails.consumeFrom
: r.requestFeesDetails.winningBid.transmitter;
feesManager__().transferCredits(watcherFeesPayer, address(this), totalFees);
IERC20(address(feesManager__())).transferFrom(watcherFeesPayer, address(this), totalFees);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Fee transfer will fail: RequestHandler is not an approved/privileged ERC20 spender

Calling transferFrom on the credits ERC20 from RequestHandler will require allowance for RequestHandler, which is never granted. _isWatcher(msg.sender) logic in Credit.sol won’t apply because msg.sender there is RequestHandler, not the Watcher.

Fix options:

  • Whitelist RequestHandler as a privileged spender in Credit.sol’s transferFrom override (auto-approve when msg.sender == watcher__().requestHandler__()).
  • Or avoid ERC20 transferFrom here and use a FeesManager method to block/debit credits atomically (preferred if available).

Minimal change (whitelist RequestHandler in Credit.sol):

// In Credit.sol -> transferFrom override
-    if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable();
-
-    if (_isWatcher(msg.sender)) _approve(from_, msg.sender, amount_);
+    if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable();
+    // Allow Watcher and RequestHandler to pull without prior allowance
+    if (_isWatcher(msg.sender) || msg.sender == address(watcher__().requestHandler__())) {
+        _approve(from_, msg.sender, amount_);
+    }
     return super.transferFrom(from_, to_, amount_);

Also confirm the destination (address(this)) is correct and won’t trap credits without a settlement path.


🏁 Script executed:

#!/bin/bash
# Locate Credit.sol
rg -l "contract Credit" -g '*.sol'

# Inspect transferFrom override in Credit.sol
rg -n -C5 "function transferFrom" -g '*Credit.sol'

# Inspect isWatcher usage in Credit.sol
rg -n "isWatcher" -g '*Credit.sol'

# Inspect watcher__() accessor in Credit.sol
rg -n "function watcher__" -g '*Credit.sol'

Length of output: 1057


🏁 Script executed:

#!/bin/bash
# Dump context around transferFrom override
sed -n '180,260p' contracts/evmx/fees/Credit.sol

# Search for watcher__ and requestHandler references
rg -n "watcher__" -g 'contracts/evmx/fees/Credit.sol'
rg -n "requestHandler" -g 'contracts/evmx/fees/Credit.sol'

Length of output: 3295


🏁 Script executed:

#!/bin/bash
# Locate requestHandler__ accessor in watcher contracts
rg -n "requestHandler__" -g 'contracts/evmx/watcher/*.sol'

Length of output: 1612


Whitelist RequestHandler in Credit.sol or use FeesManager debit
Transfer will revert: Credit.sol’s transferFrom override only auto-approves watchers, not the RequestHandler (msg.sender).
Fix options:

  • In contracts/evmx/fees/Credit.sol’s transferFrom, change:
    - if (_isWatcher(msg.sender)) _approve(from_, msg.sender, amount_);
    + if (_isWatcher(msg.sender) || msg.sender == address(watcher__().requestHandler__())) _approve(from_, msg.sender, amount_);
  • Or replace IERC20(...).transferFrom in RequestHandler.sol with a FeesManager debit method to atomically block/debit credits.
    Also confirm address(this) won’t trap credits without a settlement path.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +62 to 84
SOLANA_MAINNET = ChainId.SOLANA_MAINNET,
SOLANA_DEVNET = ChainId.SOLANA_DEVNET,
SONIC = ChainId.SONIC,
KATANA = ChainId.KATANA,
HYPEREVM = ChainId.HYPEREVM,
SEI = ChainId.SEI,
ZERO = ChainId.ZERO,
ZKSYNC = ChainId.ZKSYNC,
ARENA_Z = ChainId.ARENA_Z,
INK = ChainId.INK,
BERA = ChainId.BERA,
B3 = ChainId.B3,
UNICHAIN = ChainId.UNICHAIN,
MONAD_TESTNET = ChainId.MONAD_TESTNET,
SCROLL = ChainId.SCROLL,
SONEIUM = ChainId.SONEIUM,
SWELLCHAIN = ChainId.SWELLCHAIN,
WORLD_CHAIN = ChainId.WORLD_CHAIN,
FLOW = ChainId.FLOW,
CAMP = ChainId.CAMP,
PLUME = ChainId.PLUME,
RISE_TESTNET = ChainId.RISE_TESTNET,
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify new slugs are fully wired (ChainId and mappings).

These slugs must exist in ChainId and be present in chainSlugToId/chainSlugToHardhatChainName mappings. Missing entries will break deployments and lookups.


🏁 Script executed:

#!/bin/bash
# Ensure each new slug exists in ChainId and mapping files.
set -euo pipefail

NEW_SLUGS=(
  SOLANA_MAINNET SOLANA_DEVNET SONIC KATANA HYPEREVM SEI ZERO ZKSYNC ARENA_Z INK
  BERA B3 UNICHAIN MONAD_TESTNET SCROLL SONEIUM SWELLCHAIN WORLD_CHAIN FLOW CAMP
  PLUME RISE_TESTNET
)

echo "Checking ChainId definitions..."
for s in "${NEW_SLUGS[@]}"; do
  rg -nC2 -g 'src/**' "enum\\s+ChainId|\\b$s\\b" src/chain-enums/chainId.ts >/dev/null || echo "Missing in ChainId: $s"
done

echo "Checking chainSlugToId mapping..."
for s in "${NEW_SLUGS[@]}"; do
  rg -nC1 -g 'src/**' "ChainSlug\\.$s\\s*:" src/chain-enums/chainSlugToId.ts >/dev/null || echo "Missing in chainSlugToId: $s"
done

echo "Checking chainSlugToHardhatChainName mapping..."
for s in "${NEW_SLUGS[@]}"; do
  rg -nC1 -g 'src/**' "ChainSlug\\.$s\\s*:" src/chain-enums/chainSlugToHardhatChainName.ts >/dev/null || echo "Missing in chainSlugToHardhatChainName: $s"
done

Length of output: 1919


Define new ChainSlugs in mapping files
All slugs added in src/chain-enums/chainSlug.ts must be defined in src/chain-enums/chainSlugToId.ts and src/chain-enums/chainSlugToHardhatChainName.ts or lookups will break. Add entries for:
SOLANA_MAINNET, SOLANA_DEVNET, SONIC, KATANA, HYPEREVM, SEI, ZERO, ZKSYNC, ARENA_Z, INK, BERA, B3, UNICHAIN, MONAD_TESTNET, SCROLL, SONEIUM, SWELLCHAIN, WORLD_CHAIN, FLOW, CAMP, PLUME, RISE_TESTNET.

🤖 Prompt for AI Agents
In src/chain-enums/chainSlug.ts around lines 62 to 84, multiple new chain slugs
were added but corresponding entries are missing in
src/chain-enums/chainSlugToId.ts and
src/chain-enums/chainSlugToHardhatChainName.ts which will break lookups; add
mapping entries for each of the following slugs in both files: SOLANA_MAINNET,
SOLANA_DEVNET, SONIC, KATANA, HYPEREVM, SEI, ZERO, ZKSYNC, ARENA_Z, INK, BERA,
B3, UNICHAIN, MONAD_TESTNET, SCROLL, SONEIUM, SWELLCHAIN, WORLD_CHAIN, FLOW,
CAMP, PLUME, RISE_TESTNET, ensuring each slug maps to the correct ChainId
numeric value in chainSlugToId.ts and to the proper hardhat network name (or
null/undefined if not applicable) in chainSlugToHardhatChainName.ts; follow
existing mapping format and ordering, run tests, and update any types or exports
if required.

Comment on lines 55 to 57
uint256 withdrawAmount = feesManager.balanceOf(transmitterEOA);
withdrawAmount = withdrawAmount - feesAmount;
withdrawCredits(transmitterEOA, withdrawAmount);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid potential underflow on withdrawAmount

withdrawAmount = balanceOf(transmitterEOA) - feesAmount will revert if balance <= feesAmount. This should guard or pre-seed sufficient balance to make the test deterministic.

Apply one of:

  • Assert and seed: ensure balance > feesAmount and mint/deposit to transmitter if needed.
  • Guard and skip: if balance <= feesAmount, set withdrawAmount = 0 and expect revert accordingly.
🤖 Prompt for AI Agents
In test/evmx/FeesTest.t.sol around lines 55-57, the subtraction withdrawAmount =
balanceOf(transmitterEOA) - feesAmount can underflow if the balance is <=
feesAmount; either ensure the test pre-seeds the transmitterEOA with sufficient
funds (mint/deposit so balance > feesAmount) and assert that precondition before
computing withdrawAmount, or guard the computation by checking the balance and
setting withdrawAmount = 0 (and adjust the test to expect a revert or skip the
withdraw) to make the test deterministic and avoid underflow.

Comment on lines +787 to +793
uint256 expiryTime = expiryTime + 100;
hoax(watcherAddress);
vm.expectEmit(true, true, true, true);
emit SchedulePrecompile.ExpiryTimeSet(expiryTime);
schedulePrecompile.setExpiryTime(expiryTime);

hoax(nonOwner);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Shadowed variable uses itself before initialization

uint256 expiryTime = expiryTime + 100; reads the uninitialized local. This should reference the state var via a different local name.

-    uint256 expiryTime = expiryTime + 100;
+    uint256 newExpiryTime = expiryTime + 100;
 ...
-    emit SchedulePrecompile.ExpiryTimeSet(expiryTime);
-    schedulePrecompile.setExpiryTime(expiryTime);
+    emit SchedulePrecompile.ExpiryTimeSet(newExpiryTime);
+    schedulePrecompile.setExpiryTime(newExpiryTime);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uint256 expiryTime = expiryTime + 100;
hoax(watcherAddress);
vm.expectEmit(true, true, true, true);
emit SchedulePrecompile.ExpiryTimeSet(expiryTime);
schedulePrecompile.setExpiryTime(expiryTime);
hoax(nonOwner);
uint256 newExpiryTime = expiryTime + 100;
hoax(watcherAddress);
vm.expectEmit(true, true, true, true);
emit SchedulePrecompile.ExpiryTimeSet(newExpiryTime);
schedulePrecompile.setExpiryTime(newExpiryTime);
hoax(nonOwner);
🤖 Prompt for AI Agents
In test/evmx/Watcher.t.sol around lines 787 to 793, the line "uint256 expiryTime
= expiryTime + 100;" reads the local variable before it's initialized; replace
it by reading the contract/state variable into a new local name (e.g., uint256
newExpiry = expiryTime + 100) and use that new local for the expectEmit and
setExpiryTime calls so the state value is referenced correctly and the local is
initialized.

Comment on lines +20 to +31
function initSocket(
bytes32 appGatewayId_,
address socket_,
uint64 switchboardId_
) external override {
appGatewayId = appGatewayId_;
socket__ = ISocket(socket_);
initSocketCalled = true;

socket__.connect(appGatewayId_, switchboardId_);
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Set switchboardId state in initSocket

switchboardId storage var is never updated, causing stale/zero reads. This should assign it when initializing.

 function initSocket(
     bytes32 appGatewayId_,
     address socket_,
     uint64 switchboardId_
 ) external override {
     appGatewayId = appGatewayId_;
     socket__ = ISocket(socket_);
+    switchboardId = switchboardId_;
     initSocketCalled = true;

     socket__.connect(appGatewayId_, switchboardId_);
 }
```<!-- review_comment_end -->

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    function initSocket(
        bytes32 appGatewayId_,
        address socket_,
        uint64 switchboardId_
    ) external override {
        appGatewayId = appGatewayId_;
        socket__ = ISocket(socket_);
        switchboardId = switchboardId_;
        initSocketCalled = true;

        socket__.connect(appGatewayId_, switchboardId_);
    }
🤖 Prompt for AI Agents
In test/mock/MockPlug.sol around lines 20 to 31, the initSocket function sets
appGatewayId and socket__ but never assigns the switchboardId storage variable,
leaving it stale/zero; update the function to assign switchboardId =
switchboardId_ (ensure the storage variable name matches the contract field) so
the initialized value is persisted before calling socket__.connect.

Comment on lines +1385 to +1393
payloadPointer =
(uint160(srcSocketConfig_.chainSlug) << 120) |
(uint160(uint64(uint256(triggerId))) << 80) |
payloadCounter;

bytes32 payloadId = createPayloadId(
payloadPointer,
dstSocketConfig_.messageSwitchboard.switchboardId(),
dstSocketConfig_.chainSlug
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

payloadPointer packing uses 64-bit field for a 40-bit slot (overwrites high bits)

The middle slot is 40 bits, but you shift a 64-bit value into it, corrupting the high 24 bits and overlapping the top slot. This should constrain to 40 bits.

-        payloadPointer =
-            (uint160(srcSocketConfig_.chainSlug) << 120) |
-            (uint160(uint64(uint256(triggerId))) << 80) |
-            payloadCounter;
+        payloadPointer =
+            (uint160(srcSocketConfig_.chainSlug) << 120) |
+            (uint160(uint40(uint64(uint256(triggerId)))) << 80) |
+            uint160(payloadCounter);

Optionally add assertions to ensure values fit their bit ranges.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
payloadPointer =
(uint160(srcSocketConfig_.chainSlug) << 120) |
(uint160(uint64(uint256(triggerId))) << 80) |
payloadCounter;
bytes32 payloadId = createPayloadId(
payloadPointer,
dstSocketConfig_.messageSwitchboard.switchboardId(),
dstSocketConfig_.chainSlug
payloadPointer =
(uint160(srcSocketConfig_.chainSlug) << 120) |
(uint160(uint40(uint64(uint256(triggerId)))) << 80) |
uint160(payloadCounter);
bytes32 payloadId = createPayloadId(
payloadPointer,
dstSocketConfig_.messageSwitchboard.switchboardId(),
dstSocketConfig_.chainSlug

Comment on lines +12 to +69
contract IdUtilsTest is Test {
function testCreatePayloadId() public pure {
uint160 payloadPointer = 12345;
uint64 switchboardId = 67890;
uint32 chainSlug = 1;

bytes32 payloadId = createPayloadId(payloadPointer, switchboardId, chainSlug);

// Verify the structure
uint32 chainSlugFromId = uint32(uint256(payloadId) >> 224);
uint64 switchboardIdFromId = uint64(uint256(payloadId) >> 160);
uint160 payloadPointerFromId = uint160(uint256(payloadId));

assertEq(chainSlugFromId, chainSlug, "Chain slug should match");
assertEq(switchboardIdFromId, switchboardId, "Switchboard ID should match");
assertEq(payloadPointerFromId, payloadPointer, "Payload pointer should match");
}

function testCreatePayloadIdWithZeroValues() public pure {
bytes32 payloadId = createPayloadId(0, 0, 0);

assertEq(payloadId, bytes32(0), "Payload ID should be zero for zero inputs");
}

function testCreatePayloadIdWithMaxValues() public pure {
uint160 maxPayloadPointer = type(uint160).max;
uint64 maxSwitchboardId = type(uint64).max;
uint32 maxChainSlug = type(uint32).max;

bytes32 payloadId = createPayloadId(maxPayloadPointer, maxSwitchboardId, maxChainSlug);

// Verify the structure
uint32 chainSlugFromId = uint32(uint256(payloadId) >> 224);
uint64 switchboardIdFromId = uint64(uint256(payloadId) >> 160);
uint160 payloadPointerFromId = uint160(uint256(payloadId));

assertEq(chainSlugFromId, maxChainSlug, "Chain slug should match");
assertEq(switchboardIdFromId, maxSwitchboardId, "Switchboard ID should match");
assertEq(payloadPointerFromId, maxPayloadPointer, "Payload pointer should match");
}

function testCreatePayloadIdFuzz(
uint160 payloadPointer,
uint64 switchboardId,
uint32 chainSlug
) public pure {
bytes32 payloadId = createPayloadId(payloadPointer, switchboardId, chainSlug);

// Verify the structure
uint32 chainSlugFromId = uint32(uint256(payloadId) >> 224);
uint64 switchboardIdFromId = uint64(uint256(payloadId) >> 160);
uint160 payloadPointerFromId = uint160(uint256(payloadId));

assertEq(chainSlugFromId, chainSlug, "Chain slug should match");
assertEq(switchboardIdFromId, switchboardId, "Switchboard ID should match");
assertEq(payloadPointerFromId, payloadPointer, "Payload pointer should match");
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Remove pure from test functions (compile error)

Test functions call assertions/cheatcodes/external calls; marking them pure is invalid and will not compile. This should be non-pure public.

-contract IdUtilsTest is Test {
-    function testCreatePayloadId() public pure {
+contract IdUtilsTest is Test {
+    function testCreatePayloadId() public {
@@
-    function testCreatePayloadIdWithZeroValues() public pure {
+    function testCreatePayloadIdWithZeroValues() public {
@@
-    function testCreatePayloadIdWithMaxValues() public pure {
+    function testCreatePayloadIdWithMaxValues() public {
@@
-    function testCreatePayloadIdFuzz(
+    function testCreatePayloadIdFuzz(
         uint160 payloadPointer,
         uint64 switchboardId,
         uint32 chainSlug
-    ) public pure {
+    ) public {
@@
-contract ConvertersTest is Test {
-    function testToBytes32Format() public pure {
+contract ConvertersTest is Test {
+    function testToBytes32Format() public {
@@
-    function testToBytes32FormatWithZeroAddress() public pure {
+    function testToBytes32FormatWithZeroAddress() public {
@@
-    function testFromBytes32Format() public pure {
+    function testFromBytes32Format() public {
@@
-    function testFromBytes32FormatWithZeroAddress() public pure {
+    function testFromBytes32FormatWithZeroAddress() public {
@@
-    function testFromBytes32FormatWithInvalidAddress() public {
+    function testFromBytes32FormatWithInvalidAddress() public {
@@
-    function testFromBytes32FormatWithMaxValidAddress() public pure {
+    function testFromBytes32FormatWithMaxValidAddress() public {
@@
-    function testConvertersRoundTrip() public pure {
+    function testConvertersRoundTrip() public {
@@
-    function testConvertersFuzz(address addr) public pure {
+    function testConvertersFuzz(address addr) public {

Keep fromBytes32FormatWrapper as external pure.

Also applies to: 75-149

🤖 Prompt for AI Agents
In test/Utils.t.sol around lines 12-69 (and also apply the same change to lines
75-149), the test functions are declared as `public pure` which causes
compilation errors because tests use assertions/cheatcodes/external calls;
remove the `pure` specifier so the functions are plain `public` (i.e., change
signatures from `function ... public pure` to `function ... public`). Keep any
helper `fromBytes32FormatWrapper` as `external pure` as noted.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
package.json (1)

25-25: Guard .env load and avoid non-POSIX source.

npm scripts run under /bin/sh, so source is undefined and the command fails, and it still crashes whenever .env is absent. This should wrap the load in a file check and run inside bash: bash -c '[[ -f .env ]] && source .env; bash trace.sh'.

🧹 Nitpick comments (2)
test/apps/app-gateways/counter/CounterAppGateway.sol (2)

59-67: Remove unnecessary blank lines.

Lines 65 and 67 add unnecessary whitespace. Remove them for cleaner code.

     function incrementCounters(address[] memory instances_) public async {
         incremented = false;
         // the increase function is called on given list of instances
         for (uint256 i = 0; i < instances_.length; i++) {
             ICounter(instances_[i]).increase();
         }
-
         onCompleteData = abi.encodeWithSelector(this.onIncrementComplete.selector);
-    }
+    }

168-171: Improve parameter naming and use custom errors.

This function has multiple issues:

  1. The first four parameters are unnamed, making the function signature unclear. Name them for better readability.
  2. The revert uses a string message instead of a custom error, which is less gas-efficient.
-    function onTransfer(uint32, address, uint256, uint256, bytes memory data_) public {
+    function onTransfer(
+        uint32 chainSlug_,
+        address token_,
+        uint256 amount_,
+        uint256 msgId_,
+        bytes memory data_
+    ) public {
-        if (msg.sender != address(feesManager__())) revert("Only fees manager");
+        if (msg.sender != address(feesManager__())) revert OnlyFeesManager();
         feesManagerSwitch = abi.decode(data_, (bool));
     }

You'll also need to define the custom error at the contract level:

error OnlyFeesManager();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 40be373 and a332094.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (3)
  • hardhat-scripts/s3Config/buildConfig.ts (3 hunks)
  • package.json (4 hunks)
  • test/apps/app-gateways/counter/CounterAppGateway.sol (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • hardhat-scripts/s3Config/buildConfig.ts
🔇 Additional comments (1)
test/apps/app-gateways/counter/CounterAppGateway.sol (1)

69-73: Verify the behavior of calling async functions from non-async context.

This function calls incrementCounters, which is marked async, but incrementCountersMultiple itself is not. In the actual system, calling an async function from a non-async context might not work as expected. Verify that this pattern functions correctly for test purposes.

Comment on lines +75 to 77
function onIncrementComplete() public {
incremented = true;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add access control to callback function.

This callback lacks the onlyPromises modifier, allowing anyone to call it and set incremented = true. Other callbacks in this contract (like setCounterValues at line 105 and resolveSchedule at line 130) properly use onlyPromises. This should follow the same pattern.

-    function onIncrementComplete() public {
+    function onIncrementComplete() public onlyPromises {
         incremented = true;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function onIncrementComplete() public {
incremented = true;
}
function onIncrementComplete() public onlyPromises {
incremented = true;
}
🤖 Prompt for AI Agents
In test/apps/app-gateways/counter/CounterAppGateway.sol around lines 75 to 77,
the onIncrementComplete callback is missing the onlyPromises access-control
modifier and therefore anyone can call it to set incremented = true; update the
function signature to include the onlyPromises modifier (e.g., change to
function onIncrementComplete() public onlyPromises) so it follows the same
access control pattern used by setCounterValues and resolveSchedule and prevent
unauthorized callers from toggling incremented.

@ameeshaagrawal ameeshaagrawal changed the base branch from master to staging October 13, 2025 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants